from __future__ import annotations

import typing as t
from datetime import datetime
from datetime import timedelta

from app.models import User
from fastapi.security import OAuth2PasswordBearer
from jose import ExpiredSignatureError
from jose import jwt
from jose import JWTError
from passlib.context import CryptContext
from pydantic import BaseModel
from pydantic import ValidationError
from settings import Config
from starlette.requests import Request
from strawberry import BasePermission
from strawberry.types import Info

from .errors import ApiError
from .errors import Error

# to get a string like this run:
# openssl rand -hex 32


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
TOKEN_TYPE = "bearer"


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str):
    return pwd_context.hash(password)


async def authenticate_user(username: str, password: str) -> t.Union[bool, User]:
    user = await User.get_user_by_name(username)
    if not user:
        return False
    if not verify_password(password, user.password):
        return False
    return user


def create_access_token(data: dict, expires_delta: t.Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, Config.SECRET_KEY, algorithm=Config.ALGORITHM)
    return encoded_jwt


def parse_token_func(token: str) -> t.Optional[dict]:
    if not token:
        return None
    try:
        payload = jwt.decode(token, Config.SECRET_KEY, algorithms=[Config.ALGORITHM])
    except ExpiredSignatureError:
        raise ApiError(Error.NEED_LOGIN)
    except JWTError:
        return None
    return payload


class TokenUser(BaseModel):
    id: int
    username: str
    permissions: t.List[int] = []
    exp: int


def get_token_user(
    request: Request, parse_token: bool = False
) -> t.Optional[TokenUser]:
    if not parse_token:
        return getattr(request.state, "token_user", None)
    else:
        token = request.headers.get("token")
        payload = parse_token_func(token) or {}
        try:
            token_user = TokenUser(**payload)
        except ValidationError:
            raise ApiError(Error.NEED_LOGIN)
        else:
            request.state.token_user = token_user
    return request.state.token_user


class Login(BasePermission):
    message = ApiError(Error.NEED_LOGIN)

    # This method can also be async!
    async def has_permission(self, source: t.Any, info: Info, **kwargs) -> bool:

        request = info.context["request"]
        try:
            token_user = get_token_user(request, parse_token=True)
            user_obj = User.create_without_data(auto_id=token_user.id)
            await user_obj.refresh_from_db()
        except ApiError:
            user_obj = None
        request.state.user = user_obj
        request.state.authenticated = True if user_obj else False
        return request.state.authenticated
